package com.spzx.order.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.spzx.cart.api.RemoteCartService;
import com.spzx.cart.api.domain.CartInfo;
import com.spzx.common.core.constant.SecurityConstants;
import com.spzx.common.core.domain.R;
import com.spzx.common.core.exception.ServiceException;
import com.spzx.common.core.utils.StringUtils;
import com.spzx.common.rabbit.constant.MqConst;
import com.spzx.common.rabbit.service.RabbitService;
import com.spzx.common.security.utils.SecurityUtils;
import com.spzx.order.api.domain.OrderInfo;
import com.spzx.order.api.domain.OrderItem;
import com.spzx.order.domain.OrderLog;
import com.spzx.order.form.OrderForm;
import com.spzx.order.mapper.OrderInfoMapper;
import com.spzx.order.mapper.OrderItemMapper;
import com.spzx.order.mapper.OrderLogMapper;
import com.spzx.order.service.IOrderInfoService;
import com.spzx.order.vo.TradeVo;
import com.spzx.product.api.RemoteProductService;
import com.spzx.product.api.domain.ProductSkuVo;
import com.spzx.product.api.domain.SkuLockVo;
import com.spzx.product.api.domain.SkuPriceVo;
import com.spzx.user.api.RemoteUserAddressService;
import com.spzx.user.api.domain.UserAddress;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * <p>
 * 订单 服务实现类
 * </p>
 *
 * @author atguigu
 * @since 2024-12-26
 */
@Service
public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements IOrderInfoService {

    @Autowired
    private RemoteCartService cartService;

    @Autowired
    private RemoteProductService productService;

    @Autowired
    private RemoteUserAddressService userAddressService;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private OrderItemMapper orderItemMapper;

    @Autowired
    private OrderLogMapper orderLogMapper;

    @Autowired
    private RabbitService rabbitService;

    /**
     * 结算订单
     * @return 交易商品对象Vo
     */
    @Override
    public TradeVo orderTradeData() {
        //1.我们要结算订单，先去获取用户提交过来的选中的购物车商品
        R<List<CartInfo>> cartListResult = cartService.getCartCheckedList(SecurityConstants.INNER);
        // 获取返回的data结果
        List<CartInfo> cartInfoList = cartListResult.getData();
        //2.判断是否发生错误
        if (R.FAIL == cartListResult.getCode()){
            throw new ServiceException(cartListResult.getMsg());
        }
        //3.判断购物车是否有商品,
        if (CollectionUtils.isEmpty(cartInfoList)){
            throw new ServiceException("购物车中没有选中的商品");
        }
        //4.我们现在手里有一个List<CartInfo>，而TradeVo需要一个List<OrderItem>
        //  所以我们现在需要做类型转换，具体思路是o'r遍历集合，然后把CartInfo转换为OrderItem
        List<OrderItem> orderItemList = cartInfoList.stream()
                .map(cartInfo -> {
                    OrderItem orderItem = new OrderItem();
                    BeanUtils.copyProperties(cartInfo, orderItem);
                    return orderItem;
                }).toList();

        //5.组装TradeVo对象
        //  TradeVo 需要:1.总金额(totalAmount) 2.结算商品列表(orderItemList)
        //              3.交易号(tradeNo)     4.是否立即购买(isBuy):默认为false
        //代码执行到这，只有一个orderItemList，接下来尝试获取总金额
        //5.1 组装总金额totalAmount
        BigDecimal totalAmount = new BigDecimal(0);
        // 遍历orderItemList，获取其中的商品数量和商品价格，累加获得总金额
        for (OrderItem orderItem : orderItemList) {
            totalAmount = totalAmount.add(orderItem.getSkuPrice().multiply(new BigDecimal(orderItem.getSkuNum())));
        }
        //5.2 获取交易号 交易流水号必须是唯一的——为了防止支付页面重复提交
        String tradeNo = getAndSetTradeNo(SecurityUtils.getUserId());
        //6.铠甲合体
        TradeVo tradeVo = new TradeVo();
        tradeVo.setTradeNo(tradeNo);
        tradeVo.setTotalAmount(totalAmount);
        tradeVo.setOrderItemList(orderItemList);
        return tradeVo;
    }

    /**
     * 用户提交订单
     * @param orderForm 订单表单
     * @return 订单id
     */
    @Override
    @Transactional(rollbackFor = Exception.class) // 添加事务注解,回滚策略是遇到任何异常都回滚
    public Long submitOrder(OrderForm orderForm) {
        //获取当前用户的id
        Long userId = SecurityUtils.getUserId();
        //1.进来第一步，防止用户重复提交订单：
        //  具体思路:我们将订单流水号放到redis当中，提交订单的时候，我们先去redis中判断是否有对应key的流水号
        //          前端给我们发来的orderForm中有当前订单的流水号
        //          若redis中存在对应的流水号，那么就说明是第一次提交订单，我们允许它继续下面的操作
        //          若不存在该流水号，那么就说明是重复提交订单，我们不允许继续下面的操作(重复提交/超时过期)
        //          ps:因为我们在提交订单的时候，会执行删除流水号的操作
        //1.1 判断订单流水号是否合法，若不合法，抛出异常
        /*if (!this.checkTradeNo(userId.toString(),orderForm)){
            throw new ServiceException("请勿重复提交订单");
        }*/
        //1.2 删除订单流水号，因为要防止重复提交订单，redis中的流水号如果不存在，那么就说明订单已经提交过了
        //this.deleteTradeNo(userId.toString());
        //上述中的1.1和1.2的具体操作就是判断是否有重复提交订单，然后清除订单流水号
        //二者缺乏原子性，所以我们使用LUA脚本来保证原子性
        //获取订单流水号在redis中存储对应的key
        String userTradeKey = "user:tradeNo:" + userId;
        /*
        * 我们这样来进行判断是否重复提交的逻辑：
        * 如果我们经过查询匹配，前端传来的订单号和redis中的订单号一致
        * 那么我们就将值修改为processed，表示已经处理完毕了，最后return 1，表示成功
        *
        * 若去redis中查询对应userTradeKey的值，若返回null，那么就说明redis中不存在这个订单号
        * 这说明订单号过期了，那么我们 就return 0，表示键不存在，订单过期
        *
        * 若我们去redis中查询对应userTradeKey的值，若返回值不为null，那么就说明redis中存在这个订单号
        * 即键存在，但是值不匹配，这就表示重复提交订单了，那么我们就删除这条数据并return -1。表示订单重复提交
        */
        //1.3 使用LUA脚本优化操作
        String scriptText = """
                if redis.call('get',KEYS[1]) == ARGV[1] then
                    redis.call('set',KEYS[1],'processed','ex',ARGV[2])
                    return 1
                else
                    if redis.call('exists',KEYS[1]) == 1 then
                        redis.call('del',KEYS[1])
                        return -1
                    else
                        return 0
                    end
                end
                """;
        //执行Lua脚本，获取flag，就是我们return的值，0表示订单过期，-1表示重复提交，1表示成功
        long flag = (Long) redisTemplate.execute(
                new DefaultRedisScript<>(scriptText, Long.class),
                List.of(userTradeKey),
                orderForm.getTradeNo(),
                300);
        if (flag == 0){
            throw new ServiceException("订单已过期，请退回重试");
        }
        if (flag == -1){
            throw new ServiceException("请勿重复提交订单");
        }
        //2.获取最新价格，并且判断价格是否变化
        //2.1 首先先去前端发送给我们的封装对象中获取结算商品列表
        List<OrderItem> orderItemList = orderForm.getOrderItemList();
        //2.2 遍历这个商品列表集合，尝试获取所有的skuId集合
        List<Long> skuIdList = orderItemList.stream().map(OrderItem::getSkuId).toList();
        //2.3 通过skuId集合，我们就可以去获取(通过远程调用)这些对应skuId的商品的最新价格集合
        R<List<SkuPriceVo>> priceListResult = productService.getSkuPriceList(skuIdList,SecurityConstants.INNER);
        // 2.3.1健壮性判定
        if (R.FAIL == priceListResult.getCode()){
            throw new ServiceException(priceListResult.getMsg());
        }
        // 2.3.2 获取最新价格集合
        List<SkuPriceVo> priceList = priceListResult.getData();
        //3.将最新价格集合转换为Map类型，并且将skuId作为key，最新价格作为value
        Map<Long, BigDecimal> priceMap = priceList.stream().collect(Collectors.toMap(SkuPriceVo::getSkuId, SkuPriceVo::getSalePrice));
        //4.遍历前端发送来的商品集合，判断是否有订单价格发生了变化，我们让priceCheckResult作为判断是否有价格发生变化的基准
        String priceCheckResult = "";
        for (OrderItem orderItem : orderItemList) {
            //compareTo()方法，判断两个BigDecimal类型的数据是否相等，若相等，返回0，否则返回1
            if (orderItem.getSkuPrice().compareTo(priceMap.get(orderItem.getSkuId())) != 0){
                //4.1 进入到此处说明二者之间的价格不一致，那么我们将价格不一致的商品名做个记录
                priceCheckResult += orderItem.getSkuName() + "\n";
            }
        }
        //5.判断我们刚刚的判断基准——priceCheckResult，若不为空，那么就说明有价格发生变化
        if (!StringUtils.isEmpty(priceCheckResult)){
            //5.1 步入这里说明前端传来的商品集合中有商品发生了价格变化，那么我们就(通过远程调用)更新商品到最新价格
            //先判断是否立即购买
            if (!orderForm.getIsBuy()){
                //5.2 更新购物车中选中的商品价格
                R<Boolean> updateResult = cartService.updateCartPrice(userId, SecurityConstants.INNER);
                if (R.FAIL == updateResult.getCode()){
                    throw new ServiceException(updateResult.getMsg());
                }

            }
            throw new ServiceException("有商品价格发生了变化，请确认后支付");
        }
        //6.校验商品的库存，并且锁定库存
        //6.1 通过前端传来的orderForm对象，获取orderItemList集合，这里就是我们的购物车选中的商品集合
        //    我们需要将这些OrderItem转换为SkuLockVo对象，也就是我们需要锁定的商品对象，并归纳为集合
        List<SkuLockVo> SkuLockVoList = orderItemList.stream().map(item -> {
            SkuLockVo lockVo = new SkuLockVo();
            lockVo.setSkuId(item.getSkuId());
            lockVo.setSkuNum(item.getSkuNum());
            lockVo.setSkuName(item.getSkuName());
            return lockVo;
        }).toList();
        //6.2 获取订单的订单号
        String tradeNo = orderForm.getTradeNo();
        //6.3 远程调用，进行检验库存以及锁定库存操作
        String checkAndLockResult = productService.checkAndLock(tradeNo, SkuLockVoList, SecurityConstants.INNER).getData();
        // 若返回的字符串不为空，则说明有商品库存不足或遇到异常
        if (!StringUtils.isEmpty(checkAndLockResult)){
            throw new ServiceException(checkAndLockResult);
        }

        // 7.执行商品下单操作，并且返回订单的id
        Long orderId = null;
        try {
            //进行提交订单信息以及保存订单信息的操作
            orderId = this.saveOrder(orderForm);
        } catch (Exception e) {
            //若发生了异常错误，那么捕获异常并输出，并且取消锁定订单的库存——使用rabbitmq发送消息到指定队列，让消费者消费这条消息来进行取消锁定库存的操作
            e.printStackTrace();
            //下单失败，解锁库存
            rabbitService.sendMessage(MqConst.EXCHANGE_PRODUCT, MqConst.ROUTING_UNLOCK, orderForm.getTradeNo());
            //抛出异常
            throw new ServiceException("下单失败");
        }

        //8.删除购物车中选中的商品
        if (!orderForm.getIsBuy()){
            R<Boolean> deleteResult = cartService.deleteCartCheckedList(userId, SecurityConstants.INNER);
            if (R.FAIL == deleteResult.getCode()){
                throw new ServiceException(deleteResult.getMsg());
            }
        }

        return orderId;
    }


    /**
     * 通过前端传来的orderForm，来保存订单消息
     * @param orderForm 订单提交表单
     * @return 订单id
     */
    private Long saveOrder(OrderForm orderForm) {
        //组装orderInfo
        OrderInfo orderInfo = new OrderInfo();
        //获取当前用户的用户名
        String username = SecurityUtils.getUsername();
        //获取当前用户的id
        Long userId = SecurityUtils.getUserId();
        //组装目前为止能拿到的信息
        orderInfo.setUserId(userId);
        orderInfo.setNickName(username);
        orderInfo.setOrderNo(orderForm.getTradeNo());
        orderInfo.setRemark(orderForm.getRemark());
        orderInfo.setFeightFee(orderForm.getFeightFee());
        //远程调用spzx-user，获取用户的地址信息
        R<UserAddress> userAddressResult = userAddressService.getUserAddress(userId, SecurityConstants.INNER);
        //保证健壮性
        if (R.FAIL == userAddressResult.getCode()){
            throw new ServiceException(userAddressResult.getMsg());
        }
        //获取用户的地址信息的data内容
        UserAddress userAddress = userAddressResult.getData();
        //组装地址信息下的内容
        orderInfo.setReceiverName(userAddress.getName());
        orderInfo.setReceiverAddress(userAddress.getAddress());
        orderInfo.setReceiverPhone(userAddress.getPhone());
        orderInfo.setReceiverTagName(userAddress.getTagName());
        orderInfo.setReceiverProvince(userAddress.getProvinceCode());
        orderInfo.setReceiverCity(userAddress.getCityCode());
        orderInfo.setReceiverDistrict(userAddress.getDistrictCode());
        //获取商品的总价格
        List<OrderItem> orderItemList = orderForm.getOrderItemList();
        //通过流式计算  .reduce的含义是进行归并，
        // .reduce(BigDecimal.ZERO, BigDecimal::add)表示将流中的值，从0开始，进行累加，返回结果
        BigDecimal totalPrice = orderItemList
                .stream()
                .map(item -> item.getSkuPrice().multiply(new BigDecimal(item.getSkuNum())))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        orderInfo.setTotalAmount(totalPrice);
        orderInfo.setOriginalTotalAmount(totalPrice);
        //默认设置优惠券优惠的金额为0
        orderInfo.setCouponAmount(BigDecimal.ZERO);
        //默认设置orderInfo的状态为待付款，即为0
        orderInfo.setOrderStatus(0);
        //初始化一些默认值
        orderInfo.setCreateBy(username);
        orderInfo.setUpdateBy(username);
        //将ORDERINFO 他妈的插入数据库，这一长串傻逼玩意
        baseMapper.insert(orderInfo);
        //遍历 添加orderItem到数据库中
        for (OrderItem item : orderItemList) {
            item.setOrderId(orderInfo.getId());
            item.setCreateBy(username);
            item.setUpdateBy(username);
            orderItemMapper.insert(item);
        }
        //添加OrderLog到数据库中
        OrderLog orderLog = new OrderLog();
        orderLog.setOrderId(orderInfo.getId());
        orderLog.setProcessStatus(0);
        orderLog.setNote("提交订单");
        orderLog.setOperateUser(username);
        orderInfo.setCreateBy(username);
        orderInfo.setUpdateBy(username);
        orderLogMapper.insert(orderLog);

        return orderInfo.getId();
    }


    /**
     * 立刻购买商品
     * @param skuId 商品skuId
     * @return 交易对象
     */
    @Override
    public TradeVo buy(Long skuId) {
        //1.远程调用，获取商品skuId的信息
        R<ProductSkuVo> productSkuResult = productService.getProductSku(skuId, SecurityConstants.INNER);
        if (R.FAIL == productSkuResult.getCode()){
            throw new ServiceException(productSkuResult.getMsg());
        }

        //2.获取具体的商品信息
        ProductSkuVo productSku = productSkuResult.getData();
        //3.根据已有信息组装TradeVo
        TradeVo tradeVo = new TradeVo();
        OrderItem orderItem = new OrderItem();
        List<OrderItem> orderItemList = new ArrayList<>();
        //组装orderItem
        orderItem.setSkuNum(1);
        orderItem.setSkuId(skuId);
        orderItem.setSkuName(productSku.getSkuName());
        orderItem.setThumbImg(productSku.getThumbImg());
        orderItem.setSkuPrice(productSku.getSalePrice());
        //组装orderItemList
        orderItemList.add(orderItem);
        //组装totalAmount
        BigDecimal totalAmount = productSku.getSalePrice();
        //组装tradeNo
        String tradeNo = getAndSetTradeNo(SecurityUtils.getUserId());

        tradeVo.setTotalAmount(totalAmount);
        tradeVo.setOrderItemList(orderItemList);
        tradeVo.setTradeNo(tradeNo);
        tradeVo.setIsBuy(true);

        return tradeVo;
    }

    /**
     * 用户的【我的订单页面】的分页数据展示
     * 订单分为多种状态，因此以状态为基准，返回数据
     * @param orderStatus 订单状态
     * @return 订单列表
     */
    @Override
    public List<OrderInfo> selectUserOrderInfoList(Integer orderStatus) {
        List<OrderInfo> orderInfoList = baseMapper.selectList(new LambdaQueryWrapper<OrderInfo>()
                .eq(OrderInfo::getUserId, SecurityUtils.getUserId())
                .eq(orderStatus != null, OrderInfo::getOrderStatus, orderStatus)
                .orderByDesc(OrderInfo::getCreateTime));
        //接下来要给orderInfoList中的每一个orderInfo设置拓展字段orderItemList
        //健壮性判断
        if (!CollectionUtils.isEmpty(orderInfoList)){
            //先尝试获取OrderId集合
            List<Long> orderIdList = orderInfoList.stream().map(OrderInfo::getId).toList();
            //根据orderId集合，获取orderItemList集合
            List<OrderItem> orderItemList = orderItemMapper.selectList(new LambdaQueryWrapper<OrderItem>()
                    .in(OrderItem::getOrderId, orderIdList));
            //将orderItemList从List转换成Map,我们要让其按照orderId分组，变成Map<Long, List<OrderItem>>
            Map<Long, List<OrderItem>> map = orderItemList.stream().collect(Collectors.groupingBy(OrderItem::getOrderId));
            orderInfoList.forEach(info -> info.setOrderItemList(map.get(info.getId())));
        }
        return orderInfoList;
    }

    /**
     * 根据订单id查询订单详情信息
     * @param orderId 订单id
     * @return 订单详情
     */
    @Override
    public OrderInfo selectOrderInfoById(Long orderId) {
        OrderInfo orderInfo = baseMapper.selectById(orderId);
        List<OrderItem> orderItemList = orderItemMapper.selectList(
                new LambdaQueryWrapper<OrderItem>()
                .eq(OrderItem::getOrderId, orderId));
        orderInfo.setOrderItemList(orderItemList);
        return orderInfo;
    }

    /**
     * 根据订单id取消订单信息
     * @param orderId 订单id
     */
    @Override
    public void cancelOrder(Long orderId) {
        //传来orderId后先去判断是否有当前这个订单
        OrderInfo orderInfo = baseMapper.selectById(orderId);
        //若当前查询到的订单不为空，且状态为待付款，那么就执行删除订单操作
        if (orderInfo != null && orderInfo.getOrderStatus() == 0){
            orderInfo.setOrderStatus(-1);//已取消
            orderInfo.setCancelTime(new Date());
            orderInfo.setCancelReason("用户取消订单");
            //保存
            baseMapper.updateById(orderInfo);
            //记录日志
            OrderLog orderLog = new OrderLog();
            orderLog.setOrderId(orderId);
            orderLog.setProcessStatus(-1);
            orderLog.setCreateBy(SecurityUtils.getUsername());
            orderLog.setNote("用户取消了订单");
            //保存日志到数据库
            orderLogMapper.insert(orderLog);

            //我们删除订单之后，也要将对应商品的锁定库存给解除才对
            //发送RabbitMQ消息通知商品系统，解锁库存
            rabbitService.sendMessage(MqConst.EXCHANGE_PRODUCT, MqConst.ROUTING_UNLOCK, orderInfo.getOrderNo());
        }

    }

    /**
     * 【远程调用接口方法】
     * 根据订单号查询返回订单详情信息
     * @param orderNo
     * @return
     */
    @Override
    public OrderInfo getByOrderNo(String orderNo) {
        //根据订单号来获取订单详情信息，但是此处是从数据库中查出来的orderInfo，没有orderItemList属性
        OrderInfo orderInfo = baseMapper.selectOne(
                new LambdaQueryWrapper<OrderInfo>()
                        .eq(OrderInfo::getOrderNo, orderNo)
        );
        //我们要手动为其添加orderItemList属性
        List<OrderItem> orderItemList = orderItemMapper.selectList(
                new LambdaQueryWrapper<OrderItem>()
                .eq(OrderItem::getOrderId, orderInfo.getId())
        );
        orderInfo.setOrderItemList(orderItemList);
        return orderInfo;
    }

    /**
     * 根据订单号，修改订单状态为已支付,待发货
     * @param orderNo
     */
    @Override
    public void updateOrderStatus(String orderNo) {
        //先去获取对应订单号的订单对象
        OrderInfo orderInfo = baseMapper.selectOne(new LambdaQueryWrapper<OrderInfo>()
                .eq(OrderInfo::getOrderNo, orderNo)
                .select(OrderInfo::getId, OrderInfo::getOrderStatus));
        //幂等性判定
        if (orderInfo.getOrderStatus() == 0){
            //只有状态为待付款的订单，我们才去修改它的订单状态
            orderInfo.setOrderStatus(1);
            orderInfo.setPaymentTime(new Date());
            //进行更新
            baseMapper.updateById(orderInfo);
        }

    }


    /**
     * 删除对应订单的订单流水号
     * @param userId
     */
    /*private void deleteTradeNo(String userId) {
        String userTradeKey = "user:tradeNo:" + userId;
        redisTemplate.delete(userTradeKey);
    }*/

    /**
     * 判断订单流水号是否合法
     * @param userId
     * @param orderForm
     * @return
     */
    /*private boolean checkTradeNo(String userId, OrderForm orderForm) {
        //获取前端传来的订单号
        String tradeNo = orderForm.getTradeNo();
        //构建流水号Key
        String userTradeKey = "user:tradeNo:" + userId;
        //获取redis中存储的订单号
        String tradeNoFromRedis = (String) redisTemplate.opsForValue().get(userTradeKey);
        //二者若相等，那么就说明没有重复提交订单
        return tradeNo.equals(tradeNoFromRedis);
    }*/

    /**
     * 获取唯一的交易号/流水号
     * @param userId 用户id
     * @return 交易号
     */
    private String getAndSetTradeNo(Long userId) {
        //1.构建流水号Key
        String userTradeKey = "user:tradeNo:" + userId;
        //2.构建流水号value
        String tradeNo = UUID.randomUUID().toString().replaceAll("-", "");
        //3.将流水号存入Redis 暂存5分钟
        redisTemplate.opsForValue().set(userTradeKey, tradeNo, 5, TimeUnit.MINUTES);
        return tradeNo;
    }
}
